﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AssertPropertyChanged.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// <summary>
//   Tracks changes to objects implementing <see cref="INotifyPropertyChanged" />
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.Testing
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Linq.Expressions;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Reflection;

    /// <summary>
    /// Tracks changes to objects implementing <see cref="INotifyPropertyChanged"/>
    /// </summary>
    public class AssertPropertyChanged
    {
        #region Fields

        public static readonly object UndefinedValue = new object();
        private readonly List<PropertyChangeInfo> _changes = new List<PropertyChangeInfo>();
        private readonly INotifyPropertyChanged _source;

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="AssertPropertyChanged"/> class.
        /// </summary>
        /// <param name="source">The source.</param>
        public AssertPropertyChanged(INotifyPropertyChanged source)
        {
            _source = source;
            _source.PropertyChanged += PropertyChanged;
        }

        /// <summary>
        /// Invoke when a property on the <see cref="Source"/> object has changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        private void PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var value = GetPropertyValue(sender, e.PropertyName);
            _changes.Add(new PropertyChangeInfo(e.PropertyName, value));
        }

        /// <summary>
        /// Gets the source.
        /// </summary>
        /// <value>The source.</value>
        public INotifyPropertyChanged Source
        {
            get { return _source; }
        }

        /// <summary>
        /// Gets the property value.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns></returns>
        private static object GetPropertyValue(object obj, string propertyName)
        {
            var value = UndefinedValue;
            if (!string.IsNullOrEmpty(propertyName))
            {
                var propertyInfo = obj.GetType().GetProperties()
                    .Where(p => p.Name == propertyName)
                    .Select(p => new { Depth = p.GetDepthInInheritanceTree(), Property = p })
                    .OrderByDescending(p => p.Depth)
                    .First()
                    .Property;

                if (propertyInfo != null && propertyInfo.CanRead)
                {
                    value = propertyInfo.GetValue(obj, null);
                }
            }

            return value;
        }

        public AssertPropertyChanged Raised<TProperty>(Expression<Func<TProperty>> propertyExpression)
        {
            return Raised(propertyExpression.ExtractMemberName());
        }

        public AssertPropertyChanged Raised<TProperty>(Expression<Func<TProperty>> propertyExpression, TProperty expectedValue)
        {
            return Raised(propertyExpression.ExtractMemberName(), expectedValue);
        }

        /// <summary>
        /// Verifies the specified property change event was raised. The assertion fails if the event does not match the supplied expected values.
        /// </summary>
        /// <param name="expectedPropertyName">The expected property name.</param>
        /// <param name="expectedValue">The expected property value.</param>
        /// <returns>The current property change tracker instance.</returns>
        public AssertPropertyChanged Raised(string expectedPropertyName, object expectedValue)
        {
            Assert.AreEqual(1, MatchChangesAndRemove(expectedPropertyName, 1, c => Equals(c.Value, expectedValue)).Count(),
                            "Expected PropertyChanged event for property '{0}' was not raised.", expectedPropertyName);
            return this;
        }

        /// <summary>
        /// Verifies the specified property change event was raised. The assertion fails if the event does not match the supplied expected values.
        /// </summary>
        /// <param name="expectedPropertyName">The expected property name.</param>
        /// <returns>The current property change tracker instance.</returns>
        public AssertPropertyChanged Raised(string expectedPropertyName)
        {
            Assert.AreEqual(1, MatchChangesAndRemove(expectedPropertyName, 1, null).Count(), 
                "Expected property change for property {0}.", expectedPropertyName);
            return this;
        }

        /// <summary>
        /// Verifies the specified property change events were raised. The assertion fails if the event does not match the supplied expected values.
        /// </summary>
        /// <param name="expectedPropertyName">The expected property name.</param>
        /// <param name="expectedValues">The expected property values.</param>
        /// <returns>The current property change tracker instance.</returns>
        public AssertPropertyChanged AllRaised(string expectedPropertyName, params object[] expectedValues)
        {
            foreach (var value in expectedValues)
            {
                Raised(expectedPropertyName, value);
            }

            return this;
        }

        /// <summary>
        /// Verifies the specified property change events were raised. The assertion fails if the event does not match the supplied expected values.
        /// </summary>
        /// <param name="expectedPropertyNames">The expected property names.</param>
        /// <returns>The current property change tracker instance.</returns>
        public AssertPropertyChanged AllRaised(params string[] expectedPropertyNames)
        {
            foreach (var propertyName in expectedPropertyNames)
            {
                Raised(propertyName);
            }

            return this;
        }

        /// <summary>
        /// Verifies the specified property change event was not raised. The assertion fails if the event does not match the supplied expected values.
        /// </summary>
        /// <param name="extectedPropertyExpresssion">The extected property expresssion.</param>
        /// <returns> The current property change tracker instance.</returns>
        public AssertPropertyChanged NotRaised(Expression<Func<int>> extectedPropertyExpresssion)
        {
            return NotRaised(extectedPropertyExpresssion.ExtractMemberName());
        }

        /// <summary>
        /// Verifies the specified property change event was not raised. The assertion fails if the event does not match the supplied expected values.
        /// </summary>
        /// <param name="expectedPropertyName">Expected name of the property.</param>
        /// <returns>The current property change tracker instance.</returns>
        public AssertPropertyChanged NotRaised(string expectedPropertyName)
        {
            Assert.AreEqual(0, MatchChangesAndRemove(expectedPropertyName, 1, null).Count());
            return this;
        }

        /// <summary>
        /// Determines whether this instance is empty.
        /// </summary>
        public void IsEmpty()
        {
            if (_changes != null && _changes.Count > 0)
            {
                Assert.Fail("Unexpected property change for properties ({0}).",
                   string.Join(", ", _changes.Select(x => string.Format("\"{0}\"", x.Name)).ToArray()));
            }
        }

        /// <summary>
        /// Resets the instance.
        /// </summary>
        public void Reset()
        {
            _changes.Clear();
        }

        /// <summary>
        /// Matches the changes and remove.
        /// </summary>
        /// <param name="propertyName">The property name </param>
        /// <param name="count">The number of items to match.</param>
        /// <param name="predicate">A predicate that can be used to match additional property change information. May be null.</param>
        /// <returns>The matches changes.</returns>
        private IEnumerable<PropertyChangeInfo> MatchChangesAndRemove(string propertyName, int count,
            Func<PropertyChangeInfo, bool> predicate)
        {
            var i = 0;
            while (count > 0 && i < _changes.Count)
            {
                var change = _changes[i];
                if (change.Name == propertyName && (predicate == null || predicate(change)))
                {
                    _changes.RemoveAt(i);
                    count--;

                    yield return change;
                }
                else
                {
                    i++;
                }
            }
        }

        #region Nested type: PropertyChangeInfo

        /// <summary>
        /// Records property change information
        /// </summary>
        private class PropertyChangeInfo
        {
            #region Fields

            private readonly string _name;
            private readonly object _value;

            #endregion

            public PropertyChangeInfo(string name, object value)
            {
                _name = name;
                _value = value;
            }

            /// <summary>
            /// Gets the value.
            /// </summary>
            /// <value>The value.</value>
            public object Value
            {
                get { return _value; }
            }

            /// <summary>
            /// Gets the name.
            /// </summary>
            /// <value>The name.</value>
            public string Name
            {
                get { return _name; }
            }
        }

        #endregion
    }
}